package stegosaur.volume;

import java.util.*;
import java.io.*;
import java.security.*;

import stegosaur.CancelledException;
import stegosaur.ext.VolumeParametersValidator;


/**
 * <p>Provides an interface to create, format, manage, and delete Volumes.</p>
 * <p>TODO document me please</p>
 *
 * @author gndn.k5
 * @since Stegosaur 1.0
 */
public final class Volume
{
  /** Extension hook: volume parameters validation. **/
  private static VolumeParametersValidator volumeParametersValidator;
  
  /** List of VolumeEventListeners to notify **/
  private static List<VolumeEventListener> volumeEventListeners;
  
  /** Contains the parameters used to create this Volume. **/
  private VolumeParameters volumeParameters;

  /** A link to the default layer, which always exists. **/
  private Layer defaultLayer;

  
  /** No public constructor; enforces factory methods. **/
  private Volume() {}


  /** Extension hook: volume parameter validation **/
  public static void setVolumeParametersValidator(VolumeParametersValidator validator)
  {
    volumeParametersValidator = validator;
  }


  /**
   * Builds (or rebuilds) the index file using data from all currently visible layers.
   * If the file exists, it is deleted first.  Note that index data for any layers
   * that are not currently open will be lost in this case.
   */
  public void buildIndex() throws IOException, CancelledException
  {
    // TODO
  }


  /** Shorthand for createVolume(params, passphrase, null); */
  public static Volume createVolume(VolumeParameters params, String passphrase)
        throws VolumeParametersException, IOException, CancelledException
  {
    return createVolume(params, passphrase, null);
  }


  /**
   * Creates and formats a new volume based on the supplied VolumeParameters.
   * The passphrase specified here will be used for the default layer, which
   * is also created by this method.  The optional "listener" parameter (can be
   * null) is periodically informed of progress.
   */  
  public static Volume createVolume(VolumeParameters params, String passphrase, VolumeEventListener listener) 
        throws VolumeParametersException, IOException, CancelledException
  {
    validateVolumeParameters(params);
    
    Volume volume = new Volume();
    try
    {
      volume.volumeParameters = params;
      if (listener != null)
        volume.addVolumeEventListener(listener);
      volume.format(passphrase);
    }
    finally
    {
      if (listener != null)
        volume.removeVolumeEventListener(listener);
    }
    
    return volume;
  }


  /** Returns a copy of the VolumeParameters that were used to create this volume. **/
  public VolumeParameters getVolumeParameters()
  {
    // Return a copy so as to prevent changes to ours:
    return new VolumeParameters(volumeParameters);
  }
  

  /**
   * Formats this volume.  This involves allocating space on disk, creating the master
   * file and all split files, and creating the default layer (encoded with the specified
   * passphrase).  The datastore will initially be filled with cryptographically random noise.
   * If this method is invoked on an existing volume (as opposed to being called implicitly
   * via createVolume), then any data this volume may have contained before this call is 
   * irretrievably lost.  Also note that by default, no index file is created.  If an index
   * file already exists for this volume, it is deleted.  You must supply the passphrase which
   * will be used to encode the default layer.
   */
  public void format(String passphrase) throws IOException, CancelledException, VolumeParametersException
  {
    // If an index file exists, delete it:
    File indexFile = volumeParameters.getIndexFile();
    if (indexFile.exists())
      indexFile.delete();
    
    // Figure out how many split files we're dealing with:
    int splitFileCount = 1;
    int tempCapacity = volumeParameters.capacityMB;
    if (volumeParameters.splitFileSizeMB != 0)
    {
      while (tempCapacity > volumeParameters.splitFileSizeMB)
      {
        splitFileCount++;
        tempCapacity -= volumeParameters.splitFileSizeMB;
      }
    }
    
    // Figure out the block count per split file:
    int blockCount = 0;
    if (volumeParameters.capacityMB > volumeParameters.splitFileSizeMB && volumeParameters.splitFileSizeMB != 0)
      blockCount = (int)((volumeParameters.splitFileSizeMB*1024*1024) / volumeParameters.blockSize);
    else
      blockCount = (int)((volumeParameters.capacityMB*1024*1024) / volumeParameters.blockSize);

    // For progress events:
    int currentStep = 0;
    int totalSteps = splitFileCount * blockCount * 2;
    if (! fireVolumeFormatEvent(currentStep, totalSteps))
      throw new CancelledException("Volume creation cancelled at user request.");

    // The initial size of the master file is determined by figuring out the total number of blocks
    // in the datstore and multiplying by an arbitrary value of 64.  The master file will grow as
    // needed when inserting or rearranging data.  We cap the initial size to 1GB if necessary.
    long masterFileSize = ((long)(volumeParameters.capacityMB * 1024) / volumeParameters.blockSize) * 64;
    if (masterFileSize > (1024*1024*1024))
      masterFileSize = 1024*1024*1024;
      
    // Prepare RNGs:
    SecureRandom srand = new SecureRandom();
    srand.setSeed(new java.util.Date().getTime());
    Random rand = new Random(new java.util.Date().getTime());

    // Initialize the master file with random data:
    byte[] buffer = new byte[volumeParameters.blockSize];
    BufferedOutputStream masterFile = new BufferedOutputStream(new FileOutputStream(volumeParameters.masterFile), volumeParameters.blockSize);
    int bytesWritten = 0;
    while (bytesWritten < masterFileSize)
    {
      srand.nextBytes(buffer);
      masterFile.write(buffer);
      bytesWritten += volumeParameters.blockSize;
    }
    masterFile.flush();
    masterFile.close();

    // Format all split files:
    for (int i = 1; i <= splitFileCount; i++)
    {
      // Create split file:
      String splitFileName = volumeParameters.getSplitFileName(i);
      RandomAccessFile dataFile = new RandomAccessFile(new File(volumeParameters.masterFile.getParent(), splitFileName), "rwd");
      
      // preallocate with zeros (pads out to correct file length):
      buffer = new byte[volumeParameters.blockSize];
      for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
      {
        dataFile.write(buffer);
        if (! fireVolumeFormatEvent(++currentStep, totalSteps))
          throw new CancelledException("Volume creation cancelled at user request.");
      }
      dataFile.seek(0L);
      
      // Prepare a list of all block offsets:
      List<Long> blockList = new ArrayList<Long>(blockCount);
      for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
        blockList.add(new Long(blockIndex*(long)volumeParameters.blockSize));
            
      // Go through the list in random order:
      while (blockList.size() > 0)
      {
        // Write random data to this block:
        int index = rand.nextInt(blockList.size());
        srand.nextBytes(buffer);
        dataFile.seek(blockList.get(index));
        dataFile.write(buffer);
        blockList.remove(index);
      
        // Every once in a while, reseed the SRNG:
        if ((index % 7) == 0)
          srand.setSeed(new java.util.Date().getTime());
          
        if (! fireVolumeFormatEvent(++currentStep, totalSteps))
          throw new CancelledException("Volume creation cancelled at user request.");
      }
      
      dataFile.close();
    }
    
    createDefaultLayer(passphrase);
    
    // TODO add volume information to default layer
  }
  

  /** Internal method to create the default layer. **/  
  private void createDefaultLayer(String passphrase)
  {
    defaultLayer = new Layer(this, "Default Layer", passphrase);
    // TODO 
  }
  
  
  public Layer getDefaultLayer()
  {
    // TODO 
    return null;
  } 


  /**
   * Commits all changes to disk.
   */
  public void save() throws IOException
  {
    // TODO
  }

    
  /** Internal method to validate volume parameters during volume creation. **/
  private static void validateVolumeParameters(VolumeParameters params) throws VolumeParametersException
  {
    // Built-in validation:
    if (params.name == null || params.name.trim().equals(""))
      throw new VolumeParametersException("Volume name must be specified.");
    if (params.masterFile == null)
      throw new VolumeParametersException("Volume master file not specified.");
    if (params.masterFile.exists() || params.masterFile.isDirectory())
      throw new VolumeParametersException("Specified master file already exists.");
    if (params.splitFileSizeMB < 0 || params.splitFileSizeMB > (1024*1024))
      throw new VolumeParametersException("Invalid split file size specified.");
    params.validateSplitFileNameFormat(); 
    if ((params.blockSize % 4096) != 0 || params.blockSize < 4096 || params.blockSize > 524288)
      throw new VolumeParametersException("Block size must be a multiple of 4KB between 4KB and 512KB.");
    if (params.preferSequential != VolumeParameters.SEQUENTIAL_ALWAYS &&
        params.preferSequential != VolumeParameters.SEQUENTIAL_USUALLY &&
        params.preferSequential != VolumeParameters.SEQUENTIAL_SOMETIMES  &&
        params.preferSequential != VolumeParameters.SEQUENTIAL_RARELY &&
        params.preferSequential != VolumeParameters.SEQUENTIAL_NEVER)
      throw new VolumeParametersException("Unknown sequential preference value.");
    if (params.preferRedundant != VolumeParameters.REDUNDANT_VERY &&
        params.preferRedundant != VolumeParameters.REDUNDANT_SORTOF &&
        params.preferRedundant != VolumeParameters.REDUNDANT_NEVER)
      throw new VolumeParametersException("Unknown redundancy preference value.");
    
    // Extended validation:
    if (volumeParametersValidator != null)
      volumeParametersValidator.validate(params);
  }
  
  
  /** Subscribe to volume events (see VolumeEventListener). **/
  public void addVolumeEventListener(VolumeEventListener listener)
  {
    if (volumeEventListeners == null)
      volumeEventListeners = new java.util.ArrayList<VolumeEventListener>();
      
    volumeEventListeners.add(listener); 
  }
  
  
  /** Unsubscribe to volume events (see VolumeEventListener). **/
  public void removeVolumeEventListener(VolumeEventListener listener)
  {
    if (volumeEventListeners == null)
      return;
      
    volumeEventListeners.remove(listener);
  }
  
  
  /** Internal method to fire off a progress message related to a format in progress. **/
  private boolean fireVolumeFormatEvent(int curStep, int totalSteps)
  {
    boolean proceed = true;
    for (VolumeEventListener listener : volumeEventListeners)
    {
      proceed = listener.volumeFormatEvent(this, curStep, totalSteps);
      if (! proceed)
        break;
    }
    
    return proceed;
  }
}
